Ana içeriğe geç
  1. 100 Günde SwiftUI Notları/

49.Gün - SwiftUI Networking: Veri Gönderme Alma, AsyncImage, Form Doğrulama

Proje-10 ile SwiftUI ile networking konusunu inceleyeceğiz. Böylelikle internetten veriyi çekip projemizde kullanabileceğiz. Ayrıca uzak sunucudaki görüntüleri alabilecek ve form validation yapabileceğiz.

Proje-10 için Cupcake Corner isimli bir uygulama geliştireceğiz. Bu proje ile internetten verileri nasıl gönderip alacağımızı ve formları nasıl doğrulayacağımızı inceleyecek, ayrıca Codable’ı derinlemesine öğreneceğiz.

URLSession ve SwiftUI ile Codable Veri Gönderme ve Alma #

iOS bize internetten veri göndermek ve almak için yerleşik araçlar sunar. Bunu Codable desteği ile birleştirirsek; Swift nesnelerini göndermek için JSON’a dönüştürmek, ardından Swift nesnelerine dönüştürülmek üzere JSON’u geri almak mümkündür. Daha da iyisi, request (istek) tamamlandığında, verileri SwiftUI view property’lere hemen atayabilir ve kullanıcı arayüzünün güncellenmesini sağlayabiliriz.

Bunu göstermek için Apple’ın iTunes API’sinden bazı örnek müzik JSON verilerini yükleyebilir ve hepsini bir SwiftUI List ’de gösterebiliriz. Apple’ın verileri çok sayıda bilgi içeriyor, ancak biz bunları sadece iki türe indirgeyeceğiz: Result track ID, ismi ve ait olduğu albümü ve Response result array’i tutacak.

Dolayısıyla bu kod ile başlayabiliriz;

struct Response: Codable {
    var results: [Result]
}

struct Result: Codable {
    var trackId: Int
    var trackName: String
    var collectionName: String
}

Artık bir result array gösteren bir ContentView yazabiliriz.

struct ContentView: View {
    @State private var results = [Result]()

    var body: some View {
        List(results, id: \.trackId) { item in
            VStack(alignment: .leading) {
                Text(item.trackName)
                    .font(.headline)
                Text(item.collectionName)
            }
        }
    }
}

Bu ilk başta hiçbir şey göstermeyecektir, çünkü results array boştur. Ağ çağrımız burada devreye giriyor: iTunes API’den bize Taylor Swift’in tüm şarkılarının bir listesini göndermesini isteyeceğiz, ardından bu sonuçları bir Result array instance’ına dönüştürmek için JSONDecoder’ı kullanacağız.

Ancak bunu yapmak için iki önemli Swift keyword ile tanışmalıyız: async ve await . SwiftUI çalıştırabilen herhangi bir iPhone her saniye milyarlarca işlem gerçekleştirebilir - o kadar hızlıdır ki çoğu işi biz başladığını bile farketmeden tamamlar. Diğer taraftan, network oluşturma -internetten veri indirme- birkaç yüz milisaniye veya daha fazla sürebilir, bu da bu süre içinde milyarlarca başka şey yapmaya alışkın bir bilgisayar için son derece yavaştır.

Swift, network oluşturma işlemi gerçekleştirirken tüm ilerlememizi durdurmaya zorlamak yerine, bize “bu çalışma biraz zaman alacak, bu nedenle uygulamanın geri kalanı her zamanki gibi çalışmaya devam ederken lütfen tamamlanmasını bekleyin” deme olanağı veriyor.

Bu işlevsellik (ana uygulama kodumuz çalışmaya devam ederken bazı kodları çalışır durumda bırakma yeteneği) asynchronous (eşzamansız) fonksiyon olarak adlandırılır. Synchronous (eşzamanlı) bir fonksiyon gerektiğinde bir değer döndürmeden önce tamamen çalışan bir fonksiyondur, ancak asynchronous bir fonksiyon, devam etmeden önce başka bir işin tamamlanmasını bekleyebilmek için bir süreliğine sleep yapabilen bir fonksiyondur. Bizim durumumuzda bu, uygulamamızın geri kalanının birkaç saniye boyunca donmaması için network kodumuz gerçekleşirken sleep yapması anlamına geliyor.

Bunu anlamayı kolaylaştırmak için birkaç aşamada yazalım. ilk olarak aşağıdaki kodu ContentView’a ekleyin.

func loadData() async {

}

Buradaki yeni async keyword’e dikkat edin. Swift’e bu fonksiyonun işini tamamlamak için sleep yapmak isteyebileceğini söylüyoruz.

Bunun, List gösterilir gösterilmez çalıştırılmasını istiyoruz, ancak burada sadece onAppear() methodunu kullanamayız çünkü bu method sleep fonksiyonları nasıl ele alacağını bilmez yani fonksiyonun synchronous olmasını bekler.

SwiftUI bu tür görevler için farklı bir modifier sağlar ve ona hatırlaması kolay bir isim verir: task() . Bu, bir süreliğine sleep yapabilecek fonksiyonları çağırabilir; Swift’in bizden tek istediği, bu fonksiyonları ikinci bir keyword olan await ile işaretlemektir, böylece bir sleep olabileceğini açıkça kabul etmiş oluruz.

Bu modifier’ı şimdi List’e ekleyelim;

.task {
    await loadData()
}

İpucu: await’i try gibi düşünün. try’ın bir hata fırlatabileceğini kabul ettiğimizi söylediğimiz gibi, bir sleep olabileceğini anladığımızı söylüyoruz.

loadData() içinde tamamlamamız gereken üç adım var;

  1. Okumak istediğimiz URL’yi oluşturma (Create)
  2. Bu URL için verileri getirme (Fetch)
  3. Bu verilerin result’ını bir Response struct’a decode etme (Decode)

URL ile başlayarak bunları adım adım ekleyeceğiz. Bunun kesin bir biçime sahip olması gerekir “itunes.apple.com” ve ardından bir dizi parametre ekleyeceğiz (”iTunes Search API” web araması yaparsanız parametrelerin tamamını bulabilirsiniz) Bizim durumumuzda “Taylor Swift” arama terimini ve “song” varlığını kullanacağız, bu nedenle bunu şimdi loadData() fonksiyonuna ekleyin.

guard let url = URL(string: "https://itunes.apple.com/search?term=taylor+swift&entity=song") else {
    print("Invalid URL")
    return
}

2.adım, bu URL’den veri getirmektir, bu da sleep gerçekleşme olasılığının yüksek olduğu yerdir. “Muhtemelen” diyorum çünkü olmayabilir. iOS verileri biraz önbelleğe alır, bu nedenle URL arka arkaya iki kez getirilirse, veriler bir sleep tetiklemek yerine hemen geri gönderilir.

Ne olursa olsun, burada bir sleep mümkündür ve her sleep mümkün olduğunda çalıştırmak istediğimiz kodla birlikte await keyword kullanmamız gerekir. Daha da önemlisi, burada bir hata da fırlatılabilir, örneğin kullanıcı şu anda internete bağlı olmayabilir.

Bu nedenle, hem try hem de await’i aynı anda kullanmamız gerekir. Lütfen bu kodu önceki koddan hemen sonraya ekleyin;

do {
    let (data, _) = try await URLSession.shared.data(from: url)

    // more code to come
} catch {
    print("Invalid data")
}

Bu üç önemli şeyi ortaya çıkardı, o halde bunu parçalara ayıralım;

  1. İşimiz, bir URL alan ve bu URL’deki Data nesnesini döndüren data(from:) methodu tarafından yapılıyor. Bu method, isterseniz elle oluşturup yapılandırabileceğiniz URLSession sınıfına aittir, ancak mantıklı varsayılanlarla birlikte gelen shared bir instance’da kullanabilirsiniz.
  2. data(from:)’dan dönen değer, URL’deki verileri ve isteğin nasıl gittiğini açıklayan bazı meta verileri içeren bir tuple’dır. Meta verileri kullanmıyoruz, ancak URL’nin verilerini istiyoruz, bu nedenle alt çizgi kullandık. data için yeni bir yerel sabit oluşturuyoruz ve meta verileri atıyoruz.
  3. Aynı anda hem try hem de await kullanırken, try await yazmalıyız (await try kullanımına izin verilmez). Bunun için özel bir neden yok, ancak birini seçmek zorundaydılar, bu yüzden daha doğal okunanı seçtiler.

Dolayısıyla, indirme işlemimiz başarılı olursa data sabitimiz URL’den geri gönderilen dataya ayarlanır, ancak herhangi bir nedenle başarısız olursa kodumuz “Invalid data” yazdırır ve başka bir şey yapmaz.

Bu methodun son kısmı, Data nesnesini JSONDecoder kullanarak bir Response nesnesine dönüştürmek ve ardından içindeki results array property’ye atamaktır. Bu tam olarak daha önce kullandığımız şeydir, bu yüzden bu bir sürpriz olmamalıdır, aşağıdaki kodu // more code to come yorumunun yerine yerleştirin.

if let decodedResponse = try? JSONDecoder().decode(Response.self, from: data) {
    results = decodedResponse.results
}

Kodu çalıştırırsanız, kısa bir duraklamanın ardından Taylor Swift şarkılarının bir listesinin göründüğünü göreceksiniz.

Tüm bunlar sadece veri indirme işlemini gerçekleştirir. Bu projenin ilerleyen bölümlerinde, Codable verilerini gönderebilmeniz için biraz daha farklı bir yaklaşımın nasıl benimseneceğine bakacağız, ancak şimdilik bu kadar yeter.

SwiftUI receive json

SwiftUI ile Uzak Sunucudan Görüntü Yükleme #

SwiftUI’nin Image view’ı app bundle’da bulunan görüntüler ile harika çalışır, ancak internetten uzak bir görüntü indirmek istiyorsanız bunun yerine AsyncImage kullanmanız gerekir. Bunlar basit bir asset name veya Xcode tarafından oluşturulan bir sabit yerine bir görüntü URL’si kullanılarak oluşturulur, ancak SwiftUI geri kalan her şeyi bizim için halleder (Görüntüyü indirir, indirmeyi önbelleğe alır ve otomatik olarak sunar.)

Yani, oluşturabileceğimiz en basit görüntü şuna benzer.

AsyncImage(url: URL(string: "https://hws.dev/img/logo.png"))

Bu görüntü 1200 piksel yüksekliğindedir, ancak görüntülendiğinde çok daha büyük olduğunu göreceksiniz. Bu doğrudan AsyncImage kullanmanın temel karmaşıklıklarından birine ulaşıyor: SwiftUI, kodumuz çalıştırılıp görüntü indirilene kadar görüntü hakkında hiçbir şey bilmiyor ve bu nedenle görüntü önceden uygun şekilde boyutlandıramıyor.

AsyncImage Too Big

Projeme 1200 piksellik bir görüntü ekleyecek olsaydım, aslında bu görüntüye [email protected] adını verirdim ve ardından [email protected] şeklinde 800 piksellik bir görüntü eklerdim. SwiftUI daha sonra bizim için doğru görüntüyü yüklemeyi ve görüntünün güzel, net ve doğru boyutta görünmesini sağlamayı üstlenirdi. Bu haliyle SwiftUI bu görüntüyü 1200 piksel yüksekliğinde gösterilmek üzere tasarlamış gibi yüklüyor, yani ekranımızdan çok daha büyük olacak ve biraz da bulanık görünecek.

Bunu düzeltmek için, SwiftUI’ye önceden 3x ölçekli bir görüntüyü yüklemeye çalıştığımızı söyleyebiliriz, bunun gibi;

AsyncImage(url: URL(string: "https://hws.dev/img/logo.png"), scale: 3)

AsyncImage Scaled

Peki ya kesin bir boyut vermek isterseniz? O zaman bunu deneyerek başlayabilirsiniz

AsyncImage(url: URL(string: "https://hws.dev/img/logo.png"))
    .frame(width: 200, height: 200)

AsyncImage Frame

Bu işe yaramayacaktır, ancak belki de bu sizi şaşırtmayacak çünkü normal bir Image ile de çalışmayacaktı. Bu yüzden şu şekilde yeniden boyutlandırılabilir yapmayı deneyebilirsiniz;

AsyncImage(url: URL(string: "https://hws.dev/img/logo.png"))
    .resizable()
    .frame(width: 200, height: 200)

…. ancak bu da işe yaramayacak ve aslında daha da kötü çünkü artık kodumuz derlenmeyecek bile. Gördüğünüz gibi, burada uyguladığımız modifier’lar doğrudan SwiftUI’nin indirdiği görüntüye uygulanamazlar. Çünkü, SwiftUI görüntü verilerini gerçekten alana kadar bunları nasıl uygulayacağını bilemez.

Bunun yerine bir wrapper kullanacağız. Bu sonuçta indirilmiş görüntümüzü içerecek, ancak aynı zamanda görüntü yüklenirken kullanılacak bir placeholder da içerecektir. Uygulamanız çalıştığında placeholder’ı kısa bir süreliğine görebilirsiniz, bu 200x200 gri karedir ve indirme tamamlandığında otomatik olarak kaybolacaktır.

Görüntümüzü ayarlamak için, hazır olduğunda bize son image view’ı ileten ve daha sonra gerektiği gibi özelleştirebileceğimiz daha gelişmiş bir AsyncImage formu kullanmamız gerekir. Bonus olarak, bu bize placeholder’ı gerektiği gibi özelleştirmek için ikinci bir closure’da sağlar.

Örneğin, bitmiş image view’ın hem yeniden boyutlandırılabilir hem de sığacak şekilde ölçeklendirilebilir olmasını sağlayabilir ve daha belirgin olması için placeholder için Color.red’i kullanabiliriz.

AsyncImage(url: URL(string: "https://hws.dev/img/logo.png")) { image in
    image
        .resizable()
        .scaledToFit()
} placeholder: {
    Color.red
}
.frame(width: 200, height: 200)

AsyncImage Placeholder

Yeniden boyutlandırılabilir bir görüntü ve Color.red otomatik olarak kullanılabilir tüm alanı kaplar, bu da frame() modifier’ının artık gerçekten çalıştığı anlamına gelir.

Placeholder view istediğiniz gibi olabilir. Örneğin, Color.red öğesini ProgressView() ile değiştirirseniz, düz bir renk yerine küçük bir spinner activity elde edersiniz.

Uzak görüntünüz üzerinde tam kontrol istiyorsanız, görüntünün yüklenip yüklenmediğini, bir hatayla karşılaşıp karşılaşmadığını veya henüz tamamlanıp tamamlanmadığını bize bildiren AsyncImage oluşturmanın üçüncü bir yolu vardır. Bu özellikle indirme başarısız olduğunda özel bir view göstermek istediğiniz zamanlar için kullanışlıdır (URL mevcut değilse veya kullanıcı çevrimdışıysa vb.)

İşte kod;

AsyncImage(url: URL(string: "https://hws.dev/img/bad.png")) { phase in
    if let image = phase.image {
        image
            .resizable()
            .scaledToFit()
    } else if phase.error != nil {
        Text("There was an error loading the image.")
    } else {
        ProgressView()
    }
}
.frame(width: 200, height: 200)

AsyncImage bad image

Böylece, eğer yapabilirse görüntümüzü, indirme herhangi bir nedenle başarısız olursa bir hata mesajını veya indirme devam ederken dönen bir spinning activity göstergesini gösterecektir.

Formları Doğrulama ve Devredışı Bırakma #

SwiftUI’nin Form view’ı, kullanıcı girdisini gerçekten hızlı ve kullanışlı bir şekilde saklamamızı sağlar, ancak bazen bir adım daha ileri gitmek önemlidir, devam etmeden önce geçerli olduğundan emin olmak için bu girdiyi kontrol etmemiz gerekir.

İşte tam da bu amaç için bir modifier’ımız var: disabled() Bu modifier, kontrol etmek için bir koşul alır ve koşul doğruysa, bağlı olduğu şey kullanıcı girdisine yanıt vermez (butonlara dokunulmaz, slider sürüklenmez vs.) Burada basit property’ler kullanabilirsiniz, ancak herhangi bir koşul da iş görür : computed bir property okumak, bir method çağırmak vb.

Bunu göstermek için, username ve email adresi kabul eden bir form aşağıda verilmiştir;

struct ContentView: View {
    @State private var username = ""
    @State private var email = ""

    var body: some View {
        Form {
            Section {
                TextField("Username", text: $username)
                TextField("Email", text: $email)
            }

            Section {
                Button("Create account") {
                    print("Creating account…")
                }
            }
        }
    }
}

Bu örnekte, her iki alan da doldurulmadığı sürece kullanıcıların bir hesap oluşturmasını istemiyoruz, bu nedenle disabled() modifier’ını aşağıdaki gibi ekleyerek “Create Account” butonunu içeren form bölümünü devre dışı bırakabiliriz;

Section {
    Button("Create account") {
        print("Creating account…")
    }
}
.disabled(username.isEmpty || email.isEmpty)

Bu “username veya email boşsa bu bölüm devredışı bırakılır” anlamına gelir, ki bu tam olarak istediğimiz şeydir.

Koşulları bunun gibi ayrı bir computed property şeklinde de kullanabilirsiniz.

var disableForm: Bool {
    username.count < 5 || email.count < 5
}

Artık modifier’a bunu referans verebilirsiniz

.disabled(disableForm)

Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.

Bu yazı, SwiftUI Day 49 adresinde bulunan yazılardan kendim için aldığım notları içermektedir. Orjinal dersi takip etmek için lütfen bağlantıya tıklayın.